/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package core;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Observable;
import org.apache.log4j.Logger;

/**
 *
 * @author Sakis Vtdk
 */
public class Database extends Observable implements Serializable, Cloneable
{
	private Database()
	{
		this._version = STARTING_VERSION;

		this.addObserver(new ChangesObserver());
	}

	/**
	 * Returns the current instance of the database, or creates a new instance if there isn't already one
	 * 
	 * @return The instance of the Database
	 */
	public static Database getDatabase()
	{
		if(Database._instance == null)
		{
			Database._instance = new Database();
		}	
			
		return Database._instance;
	}

	/**
	 * Reads a database from a dbv file
	 * 
	 * @param in ObjectInputStream which will be used to load the Database
	 * @return The instance of the loaded Database
	 * 
	 * @throws ClassNotFoundException
	 * @throws IOException 
	 */
	public static Database getDatabase(ObjectInputStream in) throws ClassNotFoundException, IOException
	{
		//first nullify the instance
		_instance = null;

		//then initialize
		initialize(in);
		if(Database._instance == null)
		{
			Database._instance = new Database();
		}

		return Database._instance;
	}

	/**
	 * Initializes the instance of the Database by reading it from a file and increases
	 * the version by {@link _versionStep}
	 * 
	 * @param in ObjectInputStream from which the Database object will be read
	 * 
	 * @throws ClassNotFoundException
	 * @throws IOException 
	 */
	private static void initialize(ObjectInputStream in) throws ClassNotFoundException, IOException
	{
		_instance = (Database) in.readObject();
		_instance._version = _instance._version.add(_instance._versionStep);
		_instance.addObserver(new ChangesObserver());
	}

	@Override
	public Database clone() throws CloneNotSupportedException
	{
		Database clonedDb = (Database) super.clone(); 
		clonedDb._tables = new ArrayList<DatabaseTable>(_tables.size());

		for(DatabaseTable dbTable : _tables)
		{
			clonedDb._tables.add(dbTable.clone());
		}

		return clonedDb;
	}

	/**
	 * Creates a new database. If a previous instance of Database exists, it is stored
	 * in the _currentWorkingInstanceBackup variable so it can be retrieved later
	 * 
	 * @param databaseName The name of the new Database
	 * @return A new Database instance
	 */
	public static Database createNewDatabase(String databaseName)
	{
		//TODO: Check if we gonna need this:
		//_currentVersionInstance = _instance;

		_instance = null;
		
		Database db = getDatabase();
		db.setDatabaseName(databaseName);

		//take snapshot of the version 1.0
		takeSnapshot();

		return db;
	}

	/**
	 * Takes a snapshot of the database. <br /><br />
	 * 
	 * Snapshots are stored in a {@link HashMap} in the form [version, instance] where [version]
	 * is the base version of the current instance.
	 * <br /> <br />
	 * 
	 * <b>Warning:</b> If this method is called and there is already a snapshot 
	 * with the same baseVersion, the snapshot will be overwritten
	 */
	public static void takeSnapshot()
	{
		BigDecimal currentVersion = _instance.getCurrentVersion();

		try
		{
			_instance._snapshots.put(currentVersion, _instance.clone());
			log.debug("Snapshot taken for version: "+ currentVersion +" Total number of snapshots: "+ _instance._snapshots.size());
		}
		catch(CloneNotSupportedException ex)
		{
			log.error("Could not take snapshot for the database. Error returned: "+ ex.getMessage());
		}
	}

	/**
	 * Retrieves a snapshot of the {@link Database} based on the <code>baseVersion</code>
	 * argument. If the requested instance can not be loaded, the current database instance
	 * an exception is thrown
	 * 
	 * @param baseVersion The version of the database to be retrieved
	 * 
	 * @return The instance of the database retrieved
	 * 
	 * @throws Exception An exception is thrown when the retrieved value is null
	 */
	public static Database retrieveSnapShot(BigDecimal baseVersion) throws Exception
	{
		Database tmpDb  = _instance._snapshots.get(baseVersion);

		//if the backup of the current working instance of the database is null
		//means that this is the first we want to retrieve a snapshot
		if(_currentWorkingInstanceBackup == null)
		{
			_currentWorkingInstanceBackup = _instance;
		}

		_instance = null;
		_instance = tmpDb;

		//if the instance is null
		if(_instance == null)
		{
			//load the previous instance, and throw the exception
			_instance = _currentWorkingInstanceBackup;
			_currentWorkingInstanceBackup = null;

			throw(new Exception("Could not load the requested snapshot (version: "+ baseVersion +")"));
		}

		return _instance;
	}

	/**
	 * Gets the current working instance of the database for this session
	 * which is stored in the <code>_currentWorkingInstanceBackup</code>
	 * 
	 * If the <code>_currentWorkingInstanceBackup</code> is <code>null</code>
	 * then the <code>_instance</code> is returned
	 * 
	 * @return The working instance of the database for this session
	 */
	public static Database getCurrentWorkingInstance()
	{
		Database tmpDb = _currentWorkingInstanceBackup;

		if(tmpDb == null)
		{
			tmpDb = _instance;
		}

		return tmpDb;
	}

	/**
	 * Restores the working instance of the database of this session
	 * 
	 * Should be called after dealing with <code>retrieveSnapShot()</code> method
	 */
	public static void restoreWorkingInstance()
	{
		if(_currentWorkingInstanceBackup != null)
		{
			_instance = _currentWorkingInstanceBackup;
			_currentWorkingInstanceBackup = null;
		}
	}

	/**
	 * Adds a new table in the database
	 * 
	 * @param table The table to be added
	 * @throws Exception If the name already exists, an exception is thrown
	 */
	public void addTable(DatabaseTable table) throws Exception
	{
		if(tableExists(table.getName()))
		{
			throw new Exception("Table named: '"+ table.getName() +"' already exists.");
		}
		else
		{
			table.setId(this._tables.size());
				
			//if the table is added successfully
			if(this._tables.add(table))
			{
				//set the summary of this change
				String summary = "Table '"+ table.getName() +"' was created";

				//create the change
				Change change = new Change();
				
				change.setId(table.getId());
				change.setParentId(-1); //-1 because we refer to a table
				change.setType(Change.Type.CREATE);
				change.setObjectChanged(Change.ObjectChanged.TABLE);
				change.setChangeSummary(summary);
				change.setName(table.getName());

				//notify the observers
				setChanged();
				notifyObservers(change);

				log.info(summary);
			}
		}
	}
	
	/**
	 * Checks if a table with the given name exists in the database
	 * 
	 * @param tblName The name of the table to be checked
	 * @return true if the table exists, false if it doesn't
	 */
	public boolean tableExists(String tblName)
	{
		boolean found = false;
		for(DatabaseTable dbTable : this._tables)
		{
			if(dbTable.getName().equalsIgnoreCase(tblName))
			{
				found = true;
				break;
			}
		}
		return found;
	}

	/**
	 * Renames a table
	 * 
	 * @param tableId The id of the table to be renamed
	 * @param tableName The new name of the table
	 * 
	 * @throws Exception If there is already a table with that name, an Exception is thrown
	 * @throws ArrayIndexOutOfBoundsException If given tableId is not correct (based on the ArrayList of tables)
	 * 			an ArrayIndexOutOfBoundsException is thrown.
	 */
	public void renameTable(int tableId, String tableName) throws Exception, ArrayIndexOutOfBoundsException
	{
		
		if(tableExists(tableName))
		{
			throw new Exception ("There is already a table with the name '"+ tableName +"' !"); 
		}
		else
		{
			//check the tableId
			if(tableId < 0 || tableId > this._tables.size()-1)
			{
				throw new ArrayIndexOutOfBoundsException("Table Index '"+ tableId +"' out of bounds!");
			}
			else
			{
				String oldName = this._tables.get(tableId).getName();
				this._tables.get(tableId).setName(tableName);

				String summary = "Table id: "+ tableId +" renamed from '"+ oldName +"' to '"+ tableName +"'";
				
				Change change = new Change();
				change.setParentId(-1);
				change.setId(tableId);
				change.setType(Change.Type.UPDATE);
				change.setWhatChanged(Change.WhatChanged.NAME);
				change.setObjectChanged(Change.ObjectChanged.TABLE);
				change.setOldValue(oldName);
				change.setNewValue(tableName);
				change.setChangeSummary(summary);
				change.setName(tableName);
				
				setChanged();
				notifyObservers(change);
				
				log.info(summary);
			}
		}
	}
	
	// <editor-fold desc="Getters">
	
	public String getDatabaseName()
	{
		return this._databaseName;
	}

	public DatabaseTable getTable(int i)
	{
		return this._tables.get(i);
	}

	public int getNumberOfTables()
	{
		return this._tables.size();
	}

	public ArrayList<DatabaseTable> getTables()
	{
		return this._tables;
	}

	public BigDecimal getCurrentVersion()
	{
		return this._version;
	}

	/**
	 * Gets the latest version of the Database, the version on which all changes will be made 
	 * 
	 * @return The working version of the Database
	 */
	public BigDecimal getCurrentWorkingVersionNumber()
	{
		BigDecimal currentWorkingVersion;
		if(_currentWorkingInstanceBackup != null)
		{
			currentWorkingVersion = _currentWorkingInstanceBackup.getCurrentVersion();
		}
		else
		{
			currentWorkingVersion = _version;
		}

		return currentWorkingVersion;
	}

	public static BigDecimal getVersionStep()
	{
		return _instance._versionStep;
	}

	/**
	 * Returns the value of the starting version of the database
	 * 
	 * @return The starting version
	 */
	public static BigDecimal getStartingVersion()
	{
		return _instance.STARTING_VERSION;
	}

	/**
	 * Whether or not the database has any unsaved changes
	 * 
	 * @return true if there are unsaved changes, false otherwise
	 */
	public Boolean hasUnsavedChanges()
	{
		return this._hasUnsavedChanges;
	}

	/**
	 * Calculates and returns the next version number of the database
	 * 
	 * @return The number of the next version
	 */
	public BigDecimal getNextVersion()
	{
		return this._version.add(this._versionStep);
	}

	/**
	 * Calculates and returns the previous version number of the database
	 * 
	 * @return The number of the previous version
	 */
	public BigDecimal getPreviousVersion()
	{
		return this._version.subtract(this._versionStep);
	}

	// </editor-fold>

	// <editor-fold desc="Setters">

	public void setHasUnsavedChanges(Boolean hasUnsavedChanges)
	{
		this._hasUnsavedChanges = hasUnsavedChanges;
	}

	public void setDatabaseName(String databaseName)
	{
		this._databaseName = databaseName;
	}

	/**
	 * Takes a snapshot of the database and increases the {@link _version} 
	 * attribute of the database by {@link _versionStep}.
	 */
	public void updateVersion()
	{
		takeSnapshot();
		this._version = this._version.add(this._versionStep);
	}

	// </editor-fold>

	// <editor-fold desc="Private Variables">

	/**
	 * Singleton instance
	 */
	private static Database _instance = null;

	/**
	 * Used to store the current state of the database
	 */
	private static Database _currentWorkingInstanceBackup = null;

	/**
	 * Previous snapshots of the database
	 */
	private  HashMap<BigDecimal, Database> _snapshots = new HashMap<BigDecimal, Database>();
	
	/**
	 * Name of the database
	 */
	private String _databaseName;

	/**
	 * A collection of the tables contained in this database
	 */
	private ArrayList<DatabaseTable> _tables = new ArrayList<DatabaseTable>();

	/**
	 * The previous version of the database
	 */
	//private double _previousVersion;

	/**
	 * The current version of the database
	 */
	private BigDecimal _version;

	/**
	 * The next version of the database
	 */
	//private double _nextVersion;

	/**
	 * The step between versions
	 */
	private BigDecimal _versionStep = BigDecimal.valueOf(0.1);

	/**
	 * Flag of whether or not there are unsaved changes in the database.
	 * It is set by the ChangesObserver and the XmlGenerator
	 */
	private Boolean _hasUnsavedChanges = false;

	/**
	 * The starting version of the db
	 */
	private final BigDecimal STARTING_VERSION = BigDecimal.valueOf(1.0);	

	/**
	 * The format the version will have
	 */
	//private DecimalFormat _decimalFormat = new DecimalFormat("###.##");

	/**
	 * Logger instance
	 */
	private static Logger log = Logger.getLogger(Database.class);

	/**
	 * The class' id, used for the serialization of the object
	 */
	private static final long serialVersionUID = 1L;

	// </editor-fold>
}
